package com.fary.security.config.annotation.web.configuration;

import com.fary.aop.TargetSource;
import com.fary.aop.framework.Advised;
import com.fary.beans.factory.BeanFactoryUtils;
import com.fary.beans.factory.annotation.Autowired;
import com.fary.context.ApplicationContext;
import com.fary.core.SpringException;
import com.fary.core.annotation.Order;
import com.fary.core.io.support.SpringFactoriesLoader;
import com.fary.security.authentication.*;
import com.fary.security.config.annotation.ObjectPostProcessor;
import com.fary.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import com.fary.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import com.fary.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import com.fary.security.config.annotation.web.builders.HttpSecurity;
import com.fary.security.config.annotation.web.builders.WebSecurity;
import com.fary.security.core.Authentication;
import com.fary.security.core.AuthenticationException;
import com.fary.security.core.userdetails.UserDetails;
import com.fary.security.core.userdetails.UserDetailsService;
import com.fary.security.crypto.PasswordEncoder;
import com.fary.security.web.access.intercept.FilterSecurityInterceptor;
import com.fary.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import com.fary.util.Assert;
import com.fary.util.ReflectionUtils;
import com.fary.web.accept.ContentNegotiationStrategy;
import com.fary.web.accept.HeaderContentNegotiationStrategy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.reflect.Field;
import java.util.*;

/**
 * 在WebSecurity中会分别执行其init、configure方法。
 * 在init中创建的HttpSecurity 在WebSecurity会调用其build方法
 * 来得到SecurityFilterChain(里面有一个判断
 * 是否匹配某个请求的matches方法，和一个返回多个Filter集合的getFilters方法，
 * 这些Filter就是拦截后需要被逐个执行的，所以这些Filter的创建才是关键)
 */
@Order(100)
public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
    private final Log logger = LogFactory.getLog(WebSecurityConfigurerAdapter.class);

    private ApplicationContext context;

    private ContentNegotiationStrategy contentNegotiationStrategy = new HeaderContentNegotiationStrategy();

    // 在每个安全对象创建之后需要执行后置动作的 后置动作处理器，这里的缺省值
    // 其实只是抛出异常声明IoC容器中必须存在一个ObjectPostProcessor bean：
    // 参考 @EnableWebSecurity => @EnableGlobalAuthentication => AuthenticationConfiguration => ObjectPostProcessorConfiguration
    private ObjectPostProcessor<Object> objectPostProcessor = new ObjectPostProcessor<Object>() {
        public <T> T postProcess(T object) {
            throw new IllegalStateException(ObjectPostProcessor.class.getName() + " is a required bean. Ensure you have used @EnableWebSecurity and @Configuration");
        }
    };

    // 配置 WebSecurity 需要使用到的认证配置，可以认为是全局认证配置，会通过 set 方法被自动注入,
    // 该属性会用于从IoC容器获取目标 WebSecurity/HttpSecurity 所要直接使用的 AuthenticationManager 的双亲
    // AuthenticationManager 。 该方式可能用得上，也可能用不上，要看开发人员是配置使用
    // localConfigureAuthenticationBldr 还是使用该属性用于构建目标 WebSecurity/HttpSecurity 所要直接使用的
    // AuthenticationManager 的双亲 AuthenticationManager。
    private AuthenticationConfiguration authenticationConfiguration;
    // AuthenticationManager 构建器，缺省使用 : DefaultPasswordEncoderAuthenticationManagerBuilder
    // 所有构建的 AuthenticationManager 会是目标 WebSecurity/HttpSecurity 所要直接使用的 AuthenticationManager
    private AuthenticationManagerBuilder authenticationBuilder;
    // AuthenticationManager 构建器，缺省使用 : DefaultPasswordEncoderAuthenticationManagerBuilder
    // 所要构建的 AuthenticationManagerBuilder 会是目标 WebSecurity/HttpSecurity 所要直接使用的
    // AuthenticationManager 的双亲 AuthenticationManager。 不过缺省情况下，也就是开发人员不在子类
    // 中覆盖实现 void configure(AuthenticationManagerBuilder auth) 的情况下, 该 localConfigureAuthenticationBldr
    // 不会被用于构建目标 WebSecurity/HttpSecurity 所要直接使用的 AuthenticationManager 的双亲
    // AuthenticationManager, 这种情况下的双亲 AuthenticationManager 会来自 authenticationConfiguration
    private AuthenticationManagerBuilder localConfigureAuthenticationBldr;
    // 是否禁用 localConfigureAuthenticationBldr, 缺省情况下，也就是开发人员不在子类中覆盖实现
    // void configure(AuthenticationManagerBuilder auth) 的情况下,  当前 WebSecurityConfigurerAdapter
    // 缺省提供的 void configure(AuthenticationManagerBuilder auth)  方法实现会将该标志设置为 true,
    // 也就是不使用 localConfigureAuthenticationBldr 构建目标 WebSecurity/HttpSecurity 所要直接使用的
    // AuthenticationManager 的双亲 AuthenticationManager, 而是使用 authenticationConfiguration
    // 提供的 AuthenticationManager 作为 双亲 AuthenticationManager。
    private boolean disableLocalConfigureAuthenticationBldr;
    // 标志属性 : 目标 WebSecurity/HttpSecurity 所要直接使用的AuthenticationManager的双亲 authenticationManager
    // 是否已经初始化
    private boolean authenticationManagerInitialized;
    // 目标 WebSecurity/HttpSecurity 所要直接使用的AuthenticationManager的双亲 authenticationManager
    private AuthenticationManager authenticationManager;
    // 根据传入的 Authentication 的类型判断一个 Authentication 是否可被信任,
    // 缺省使用实现机制 AuthenticationTrustResolverImpl
    // 可被设置
    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
    // HTTP 安全构建器，用于配置匹配特定URL模式的控制器方法的安全，构建产物是 DefaultSecurityFilterChain
    private HttpSecurity http;
    // 是否禁用缺省配置,缺省为 false，可以通过当前类构造函数设置为true
    private boolean disableDefaults;

    /**
     * Creates an instance with the default configuration enabled.
     */
    protected WebSecurityConfigurerAdapter() {
        this(false);
    }

    /**
     * Creates an instance which allows specifying if the default configuration should be
     * enabled. Disabling the default configuration should be considered more advanced
     * usage as it requires more understanding of how the framework is implemented.
     *
     * @param disableDefaults true if the default configuration should be disabled, else
     *                        false
     */
    protected WebSecurityConfigurerAdapter(boolean disableDefaults) {
        this.disableDefaults = disableDefaults;
    }

    /**
     *
     */
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        this.disableLocalConfigureAuthenticationBldr = true;
    }

    /**
     *
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    protected final HttpSecurity getHttp() throws Exception {
        if (http != null) {
            return http;
        }

        AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
        localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);

        AuthenticationManager authenticationManager = authenticationManager();
        authenticationBuilder.parentAuthenticationManager(authenticationManager);
        Map<Class<?>, Object> sharedObjects = createSharedObjects();

        http = new HttpSecurity(objectPostProcessor, authenticationBuilder, sharedObjects);
        if (!disableDefaults) {
            // @formatter:off
            http
                    .csrf().and()
                    .addFilter(new WebAsyncManagerIntegrationFilter())
                    .exceptionHandling().and()
                    .headers().and()
                    .sessionManagement().and()
                    .securityContext().and()
                    .requestCache().and()
                    .anonymous().and()
                    .servletApi().and()
                    .apply(new DefaultLoginPageConfigurer<>()).and()
                    .logout();
            // @formatter:on
            ClassLoader classLoader = this.context.getClassLoader();
            List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);

            for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
                http.apply(configurer);
            }
        }
        configure(http);
        return http;
    }

    /**
     * Override this method to expose the {@link AuthenticationManager} from
     * {@link #configure(AuthenticationManagerBuilder)} to be exposed as a Bean. For
     * example:
     *
     * <pre>
     * &#064;Bean(name name="myAuthenticationManager")
     * &#064;Override
     * public AuthenticationManager authenticationManagerBean() throws Exception {
     *     return super.authenticationManagerBean();
     * }
     * </pre>
     *
     * @return the {@link AuthenticationManager}
     * @throws Exception
     */
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return new AuthenticationManagerDelegator(authenticationBuilder, context);
    }

    /**
     * Gets the {@link AuthenticationManager} to use. The default strategy is if
     * {@link #configure(AuthenticationManagerBuilder)} method is overridden to use the
     * {@link AuthenticationManagerBuilder} that was passed in. Otherwise, autowire the
     * {@link AuthenticationManager} by type.
     *
     * @return the {@link AuthenticationManager} to use
     * @throws Exception
     */
    protected AuthenticationManager authenticationManager() throws Exception {
        if (!authenticationManagerInitialized) {
            configure(localConfigureAuthenticationBldr);
            if (disableLocalConfigureAuthenticationBldr) {
                authenticationManager = authenticationConfiguration.getAuthenticationManager();
            } else {
                authenticationManager = localConfigureAuthenticationBldr.build();
            }
            authenticationManagerInitialized = true;
        }
        return authenticationManager;
    }

    /**
     * Override this method to expose a {@link UserDetailsService} created from
     * {@link #configure(AuthenticationManagerBuilder)} as a bean. In general only the
     * following override should be done of this method:
     *
     * <pre>
     * &#064;Bean(name = &quot;myUserDetailsService&quot;)
     * // any or no name specified is allowed
     * &#064;Override
     * public UserDetailsService userDetailsServiceBean() throws Exception {
     * 	return super.userDetailsServiceBean();
     * }
     * </pre>
     * <p>
     * To change the instance returned, developers should change
     * {@link #userDetailsService()} instead
     *
     * @return the {@link UserDetailsService}
     * @throws Exception
     * @see #userDetailsService()
     */
    public UserDetailsService userDetailsServiceBean() throws Exception {
        AuthenticationManagerBuilder globalAuthBuilder = context.getBean(AuthenticationManagerBuilder.class);
        return new UserDetailsServiceDelegator(Arrays.asList(localConfigureAuthenticationBldr, globalAuthBuilder));
    }

    /**
     * Allows modifying and accessing the {@link UserDetailsService} from
     * {@link #userDetailsServiceBean()} without interacting with the
     * {@link ApplicationContext}. Developers should override this method when changing
     * the instance of {@link #userDetailsServiceBean()}.
     *
     * @return the {@link UserDetailsService} to use
     */
    protected UserDetailsService userDetailsService() {
        AuthenticationManagerBuilder globalAuthBuilder = context.getBean(AuthenticationManagerBuilder.class);
        return new UserDetailsServiceDelegator(Arrays.asList(localConfigureAuthenticationBldr, globalAuthBuilder));
    }

    public void init(final WebSecurity web) throws Exception {
        final HttpSecurity http = getHttp();
        web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
            FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
            web.securityInterceptor(securityInterceptor);
        });
    }

    /**
     * Override this method to configure {@link WebSecurity}. For example, if you wish to
     * ignore certain requests.
     * <p>
     * Endpoints specified in this method will be ignored by Spring Security, meaning it
     * will not protect them from CSRF, XSS, Clickjacking, and so on.
     * <p>
     * Instead, if you want to protect endpoints against common vulnerabilities, then see
     * {@link #configure(HttpSecurity)} and the {@link HttpSecurity#authorizeRequests}
     * configuration method.
     */
    public void configure(WebSecurity web) throws Exception {
    }

    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     * <p>
     * Any endpoint that requires defense against common vulnerabilities can be specified here, including public ones.
     * See {@link HttpSecurity#authorizeRequests} and the `permitAll()` authorization rule
     * for more details on public endpoints.
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    // @formatter:off
    protected void configure(HttpSecurity http) throws Exception {
        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().and()
                .httpBasic();
    }
    // @formatter:on

    /**
     * Gets the ApplicationContext
     *
     * @return the context
     */
    protected final ApplicationContext getApplicationContext() {
        return this.context;
    }

    @Autowired
    public void setApplicationContext(ApplicationContext context) {
        this.context = context;

        // 创建用于构建AuthenticationManager的对象
        // 与@AuthenticationConfiguration中十分类似
        ObjectPostProcessor<Object> objectPostProcessor = context.getBean(ObjectPostProcessor.class);
        LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(context);

        authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, passwordEncoder);
        //创建可用于在这里进行配置的AuthenticationManagerBuilder
        //该DefaultPasswordEncoderAuthenticationManagerBuilder在
        //@AuthenticationConfiguration中也使用到了
        localConfigureAuthenticationBldr = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, passwordEncoder) {
            // 设置“是否删除凭据”
            @Override
            public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) {
                authenticationBuilder.eraseCredentials(eraseCredentials);
                return super.eraseCredentials(eraseCredentials);
            }

            @Override
            public AuthenticationManagerBuilder authenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
                authenticationBuilder.authenticationEventPublisher(eventPublisher);
                return super.authenticationEventPublisher(eventPublisher);
            }
        };
    }

    @Autowired(required = false)
    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        this.trustResolver = trustResolver;
    }

    @Autowired(required = false)
    public void setContentNegotationStrategy(ContentNegotiationStrategy contentNegotiationStrategy) {
        this.contentNegotiationStrategy = contentNegotiationStrategy;
    }

    @Autowired
    public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
        this.objectPostProcessor = objectPostProcessor;
    }

    /**
     * AuthenticationConfiguration为认证配置，
     * 在@EnableWebSecurity注解里面的@EnableGlobalAuthentication中
     */
    @Autowired
    public void setAuthenticationConfiguration(AuthenticationConfiguration authenticationConfiguration) {
        this.authenticationConfiguration = authenticationConfiguration;
    }

    private AuthenticationEventPublisher getAuthenticationEventPublisher() {
        if (this.context.getBeanNamesForType(AuthenticationEventPublisher.class).length > 0) {
            return this.context.getBean(AuthenticationEventPublisher.class);
        }
        return this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher());
    }

    /**
     * Creates the shared objects
     *
     * @return the shared Objects
     */
    private Map<Class<?>, Object> createSharedObjects() {
        Map<Class<?>, Object> sharedObjects = new HashMap<>();
        sharedObjects.putAll(localConfigureAuthenticationBldr.getSharedObjects());
        sharedObjects.put(UserDetailsService.class, userDetailsService());
        sharedObjects.put(ApplicationContext.class, context);
        sharedObjects.put(ContentNegotiationStrategy.class, contentNegotiationStrategy);
        sharedObjects.put(AuthenticationTrustResolver.class, trustResolver);
        return sharedObjects;
    }

    /**
     * Delays the use of the {@link UserDetailsService} from the
     * {@link AuthenticationManagerBuilder} to ensure that it has been fully configured.
     *
     * @author Rob Winch
     * @since 3.2
     */
    static final class UserDetailsServiceDelegator implements UserDetailsService {
        private List<AuthenticationManagerBuilder> delegateBuilders;
        private UserDetailsService delegate;
        private final Object delegateMonitor = new Object();

        UserDetailsServiceDelegator(List<AuthenticationManagerBuilder> delegateBuilders) {
            if (delegateBuilders.contains(null)) {
                throw new IllegalArgumentException("delegateBuilders cannot contain null values. Got " + delegateBuilders);
            }
            this.delegateBuilders = delegateBuilders;
        }

        public UserDetails loadUserByUsername(String username) throws SpringException {
            if (delegate != null) {
                return delegate.loadUserByUsername(username);
            }

            synchronized (delegateMonitor) {
                if (delegate == null) {
                    for (AuthenticationManagerBuilder delegateBuilder : delegateBuilders) {
                        delegate = delegateBuilder.getDefaultUserDetailsService();
                        if (delegate != null) {
                            break;
                        }
                    }

                    if (delegate == null) {
                        throw new IllegalStateException("UserDetailsService is required.");
                    }
                    this.delegateBuilders = null;
                }
            }

            return delegate.loadUserByUsername(username);
        }
    }

    /**
     * Delays the use of the {@link AuthenticationManager} build from the
     * {@link AuthenticationManagerBuilder} to ensure that it has been fully configured.
     *
     * @author Rob Winch
     * @since 3.2
     */
    static final class AuthenticationManagerDelegator implements AuthenticationManager {
        private AuthenticationManagerBuilder delegateBuilder;
        private AuthenticationManager delegate;
        private final Object delegateMonitor = new Object();
        private Set<String> beanNames;

        AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) {
            Assert.notNull(delegateBuilder, "delegateBuilder cannot be null");
            Field parentAuthMgrField = ReflectionUtils.findField(AuthenticationManagerBuilder.class, "parentAuthenticationManager");
            ReflectionUtils.makeAccessible(parentAuthMgrField);
            beanNames = getAuthenticationManagerBeanNames(context);
            validateBeanCycle(ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), beanNames);
            this.delegateBuilder = delegateBuilder;
        }

        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            if (delegate != null) {
                return delegate.authenticate(authentication);
            }

            synchronized (delegateMonitor) {
                if (delegate == null) {
                    delegate = this.delegateBuilder.getObject();
                    this.delegateBuilder = null;
                }
            }

            return delegate.authenticate(authentication);
        }

        private static Set<String> getAuthenticationManagerBeanNames(ApplicationContext applicationContext) {
            String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, AuthenticationManager.class);
            return new HashSet<>(Arrays.asList(beanNamesForType));
        }

        private static void validateBeanCycle(Object auth, Set<String> beanNames) {
            if (auth != null && !beanNames.isEmpty()) {
                if (auth instanceof Advised) {
                    Advised advised = (Advised) auth;
                    TargetSource targetSource = advised.getTargetSource();
                    if (targetSource instanceof LazyInitTargetSource) {
                        LazyInitTargetSource lits = (LazyInitTargetSource) targetSource;
                        if (beanNames.contains(lits.getTargetBeanName())) {
                            throw new FatalBeanException("A dependency cycle was detected when trying to resolve the AuthenticationManager. Please ensure you have configured authentication.");
                        }
                    }
                }
                beanNames = Collections.emptySet();
            }
        }
    }

    static class DefaultPasswordEncoderAuthenticationManagerBuilder extends AuthenticationManagerBuilder {
        private PasswordEncoder defaultPasswordEncoder;

        /**
         * Creates a new instance
         *
         * @param objectPostProcessor the {@link ObjectPostProcessor} instance to use.
         */
        DefaultPasswordEncoderAuthenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor, PasswordEncoder defaultPasswordEncoder) {
            super(objectPostProcessor);
            this.defaultPasswordEncoder = defaultPasswordEncoder;
        }

        @Override
        public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication() throws Exception {
            return super.inMemoryAuthentication().passwordEncoder(this.defaultPasswordEncoder);
        }

        @Override
        public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication() throws Exception {
            return super.jdbcAuthentication().passwordEncoder(this.defaultPasswordEncoder);
        }

        @Override
        public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(T userDetailsService) throws Exception {
            return super.userDetailsService(userDetailsService).passwordEncoder(this.defaultPasswordEncoder);
        }
    }

    static class LazyPasswordEncoder implements PasswordEncoder {
        private ApplicationContext applicationContext;
        private PasswordEncoder passwordEncoder;

        LazyPasswordEncoder(ApplicationContext applicationContext) {
            this.applicationContext = applicationContext;
        }

        @Override
        public String encode(CharSequence rawPassword) {
            return getPasswordEncoder().encode(rawPassword);
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return getPasswordEncoder().matches(rawPassword, encodedPassword);
        }

        @Override
        public boolean upgradeEncoding(String encodedPassword) {
            return getPasswordEncoder().upgradeEncoding(encodedPassword);
        }

        private PasswordEncoder getPasswordEncoder() {
            if (this.passwordEncoder != null) {
                return this.passwordEncoder;
            }
            PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
            if (passwordEncoder == null) {
                passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
            }
            this.passwordEncoder = passwordEncoder;
            return passwordEncoder;
        }

        private <T> T getBeanOrNull(Class<T> type) {
            try {
                return this.applicationContext.getBean(type);
            } catch (SpringException notFound) {
                return null;
            }
        }

        @Override
        public String toString() {
            return getPasswordEncoder().toString();
        }
    }
}